/**
 * File Animation1D.java
 * ---------------------------------------------------------
 *
 * Copyright (C) 2012 David Bauske (david.bauske@googlemail.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * - The origin of the software must not be misrepresented.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * TL;DR: As long as you clearly give me credit for this Software, you are free to use as you like, even in commercial software, but don't blame me
 * 		if it breaks something.
 */
package net.cl.util.anim;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.Timer;

import net.cl.util.patterns.DefaultProvider;

/**
 * Represents a 1-dimensional animation.
 * <p>
 * {@link AnimationListener}s may be added to this class's instances to be notified
 * of the animation's progress. The callback methods in this interfaces are guaranteed
 * to be executed in the swing event dispatcher thread, so you may freely modify you UI
 * in the method's bodies.
 * </p>
 * @author David
 */
public class Animation1D extends DefaultProvider<AnimationListener>
{
	// ------------------------------------------------------------------------------
	// Constants
	// ------------------------------------------------------------------------------

	public static final double			NOT_ANIMATING			= -1.0;

	public static final double			DEFAULT_DURATION		= 0.3;
	public static final int				DEFAULT_FRAME_RATE		= 30;
	public static final Interpolator	DEFAULT_INTERPOLATOR	= EaseInOutInterpolator.INSTANCE;

	// ------------------------------------------------------------------------------
	// Fields
	// ------------------------------------------------------------------------------

	private double						mDuration				= DEFAULT_DURATION;
	private double						mProgress				= NOT_ANIMATING;
	private double						mStartValue				= 0;
	private double						mTargetValue			= 0;
	private int							mFrameRate				= DEFAULT_FRAME_RATE;
	private Interpolator				mInterpolator			= DEFAULT_INTERPOLATOR;
	private Timer						mTimer					= null;

	// ------------------------------------------------------------------------------
	// Public methods
	// ------------------------------------------------------------------------------

	/**
	 * Constructs a default animation object.
	 * <p>
	 * Default values:
	 * <ul>
	 * 	<li>Duration: {@value #DEFAULT_DURATION}s</li>
	 * 	<li>Frame rate: {@value #DEFAULT_FRAME_RATE}fps</li>
	 * 	<li>Interpolator: {@link EaseInOutInterpolator}</li>
	 * </ul>
	 * </p>
	 */
	public Animation1D()
	{

	}

	/**
	 * Constructs a new animation object.
	 *
	 * @param pDuration The duration of the new animation in seconds. Must satisfy the
	 * 			contract of {@link #setDuration(double)}.
	 * @param pFrameRate The frame rate (frames per second) of the animation.
	 * 			Must satisfy the contract of {@link #setFrameRate(int)}.
	 * @param pInterpolator The interpolator to use to interpolate between the start
	 * 		and the target value. May not be null.
	 * @throws IllegalArgumentException if pDuration or pFrameRate is null.
	 * @throws NullPointerException if pInterpolator is null.
	 */
	public Animation1D(double pDuration, int pFrameRate, Interpolator pInterpolator)
	{
		this.setDuration(pDuration);
		this.setFrameRate(pFrameRate);
		this.setInterpolator(pInterpolator);
	}

	/**
	 * Starts an animation.
	 * <p>
	 * This Method is a shortcut and equivalent to {@code start(getStartValue(), getTargetValue());}.
	 * </p>
	 */
	public void start()
	{
		this.start(this.mStartValue, this.mTargetValue);
	}

	/**
	 * Starts an animation.
	 * <p>
	 * Only one animation can run at a time per instance of Animation1D, so if there's already
	 * an animation running on this object, it will be stopped.
	 * </p>
	 * @param pStart The value to start the animation from
	 * @param pEnd The value the animation will reach at the end
	 */
	public void start(double pStart, double pEnd)
	{
		this.cancel();		// Resets any possibly ongoing animation

		this.mStartValue = pStart;
		this.mTargetValue = pEnd;
		this.mProgress = 0.0;
		this.mTimer = new Timer(mFrameRate, new Animator());
		this.mTimer.start();
	}

	/**
	 * Cancels the currently ongoing animation. Has no effect if this object is currently
	 * not animating.
	 */
	public void cancel()
	{
		if(this.mTimer != null)
		{
			this.mTimer.stop();
			this.mTimer = null;
		}
		this.mProgress = NOT_ANIMATING;
	}

	// ------------------------------------------------------------------------------
	// Bean-properties
	// ------------------------------------------------------------------------------

	/**
	 * @param pDuration the new duration in seconds of the animation. Must be
	 * 				greater or equal to zero.
	 * @throws IllegalArgumentException if pDuration is < 0.
	 */
	public void setDuration(double pDuration)
	{
		if(pDuration < 0)
		{
			throw new IllegalArgumentException("The animation's duration may not be negative.");
		}
		this.mDuration = pDuration;
	}

	/**
	 * @return The mDuration of this Animation1D object
	 */
	public double getDuration()
	{
		return mDuration;
	}

	/**
	 * @param pProgress the progress to set
	 */
	public void setProgress(double pProgress)
	{
		this.mProgress = pProgress;
	}

	/**
	 * @return The progress of this Animation or NOT_ANIMATING if there is currently
	 * 		no animation running.
	 */
	public double getProgress()
	{
		return mProgress;
	}

	/**
	 * @param pStartValue the startValue to set
	 */
	public void setStartValue(double pStartValue)
	{
		this.mStartValue = pStartValue;
	}

	/**
	 * @return The mStartValue of this Animation1D object
	 */
	public double getStartValue()
	{
		return mStartValue;
	}

	/**
	 * @param pTargetValue the targetValue to set
	 */
	public void setTargetValue(double pTargetValue)
	{
		this.mTargetValue = pTargetValue;
	}

	/**
	 * @return The mTargetValue of this Animation1D object
	 */
	public double getTargetValue()
	{
		return mTargetValue;
	}

	/**
	 * @param pInterpolator the interpolator object to set. May not be null.
	 * @throws NullPointerException if pInterpolator is null.
	 */
	public void setInterpolator(Interpolator pInterpolator)
	{
		if(pInterpolator == null)
		{
			throw new NullPointerException("pInterpolator may not be null.");
		}
		this.mInterpolator = pInterpolator;
	}

	/**
	 * @return The mInterpolator of this Animation1D object
	 */
	public Interpolator getInterpolator()
	{
		return mInterpolator;
	}

	/**
	 * Sets this animation's frame rate. Will only affect new animations, if there's
	 * already an animation running, it will run at the previous value.
	 *
	 * Default value is {@value #DEFAULT_FRAME_RATE} fps.
	 *
	 * @param pFrameRate the frameRate to set (in frames per second / fps)
	 * @throws IllegalArgumentException if pFrameRate is <= 0.
	 */
	public void setFrameRate(int pFrameRate)
	{
		if(pFrameRate <= 0)
		{
			throw new IllegalArgumentException("The frame rate must be positive.");
		}
		this.mFrameRate = pFrameRate;
	}

	/**
	 * @return The mFrameRate of this Animation1D object
	 */
	public int getFrameRate()
	{
		return mFrameRate;
	}

	// ------------------------------------------------------------------------------
	// Inner types
	// ------------------------------------------------------------------------------

	private class Animator implements ActionListener
	{
		@Override
		public void actionPerformed(ActionEvent e)
		{
			// Step the animation
			mProgress += 1.0 / mFrameRate;

			double position;
			if(mProgress < mDuration)
			{
				position = mInterpolator.getInterpolationValue(
						mProgress / mDuration,
						mStartValue,
						mTargetValue);
			}
			else
			{
				position = mTargetValue;
			}

			// Notify the animation listeners
			for(AnimationListener listener : Animation1D.this.mListeners)
			{
				listener.onProgress(Animation1D.this, position);
			}

			if(position == mTargetValue)
			{
				// Animation 'naturally' ended. Notify listeners.
				for(AnimationListener listener : Animation1D.this.mListeners)
				{
					listener.onFinished(Animation1D.this);
				}
			}

			if(mProgress >= mDuration || mProgress == NOT_ANIMATING)
			{
				// Stop this task, animation finished.
				cancel();
			}
		}
	}
}
